#ifndef RAY_TRACE_PRO_CAMERA_PROJECTION_H
#define RAY_TRACE_PRO_CAMERA_PROJECTION_H

#include "Ray.h"

namespace RayTracePro
{
	class CameraProjection
	{
	private:
		Vec3 right, up;
		float centerX, centerY;
		int screenWidth, screenHeight;
		
		float screenZ;
		friend class Renderer;
	public:
		inline float GetCenterX()
		{
			return centerX;
		}
		inline float GetCenterY()
		{
			return centerY;
		}
		inline Vec3 GetRight()
		{
			return right;
		}
		inline Vec3 GetUp()
		{
			return up;
		}
		inline float GetScreenZ()
		{
			return screenZ;
		}
		Matrix4 CameraTransform;
		CameraProjection()
		{
			Matrix4::CreateIdentityMatrix(CameraTransform);
			SetPerspective(640, 480, 45.0f);
		}
		inline int GetScreenWidth()
		{
			return screenWidth;
		}
		inline int GetScreenHeight()
		{
			return screenHeight;
		}
		
		inline void SetPerspective(int width, int height, float fovy)
		{
			screenWidth = width;
			screenHeight = height;
			screenZ = -(height>>1)/tanf(fovy*(0.5f*PI/180.0f));
			right.SetZero();
			up.SetZero();
			right.x = 1.0f;
			up.y = right.x;
			centerX = (float)(width >> 1);
			centerY = (float)(height >> 1);
		}
		static __forceinline void GenerateRay(CameraProjection & cam, Ray & r, RayDifferential & rayDiff, float x, float y)
		{
			Vec3 tmpVec;
			// Compute camera ray
			// dir = || right * px + up * py + screenZ ||
			r.tMax = FLT_MAX;
			Vec3::Scale(r.Direction, cam.right, x - cam.centerX);
			Vec3::Scale(tmpVec, cam.up, cam.centerY - y);
			r.Direction += tmpVec;
			r.Direction.z = cam.screenZ;
			Vec3 d = r.Direction;
			float dirDOTdir = Vec3::Dot(r.Direction, r.Direction);
			float dirLength = sqrt(dirDOTdir);
			float invDirLength = 1.0f/dirLength;
			r.Direction *= invDirLength;
			r.Origin.SetZero();
			Vec3 tmp;
			cam.CameraTransform.Transform(tmp, r.Origin); r.Origin = tmp;
			cam.CameraTransform.TransformNormal(tmp, r.Direction); r.Direction = tmp;
			r.ReciprocalDirection.x = 1.0f / r.Direction.x;
			r.ReciprocalDirection.y = 1.0f / r.Direction.y;
			r.ReciprocalDirection.z = 1.0f / r.Direction.z;
			// Compute initial ray differential
			rayDiff.dPdx.SetZero();
			rayDiff.dPdy.SetZero();
		
			float dirDOTdir3o2 = 1.0f / sqrt(dirDOTdir * dirDOTdir * dirDOTdir);

			Vec3::Scale(rayDiff.dDdx, cam.right, dirDOTdir);
			Vec3::Scale(tmpVec, cam.right, Vec3::Dot(d, cam.right));
			rayDiff.dDdx -= tmpVec;
			rayDiff.dDdx *= dirDOTdir3o2;

			Vec3::Scale(rayDiff.dDdy, cam.up, dirDOTdir);
			Vec3::Scale(tmpVec, cam.up, Vec3::Dot(d, cam.up));
			rayDiff.dDdy -= tmpVec;
			rayDiff.dDdy *= dirDOTdir3o2;
		}
	};
}

#endif